diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 3c488fd2d88aecdd85b6cdb5a7f1a5bc764776d3..f4ba752a6a1c376edb24e14be37271c36d64eca8 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -11827,6 +11827,7 @@ Relationship between an epic and an issue.
 | <a id="epicissueepicissueid"></a>`epicIssueId` | [`ID!`](#id) | ID of the epic-issue relation. |
 | <a id="epicissueescalationpolicy"></a>`escalationPolicy` | [`EscalationPolicyType`](#escalationpolicytype) | Escalation policy associated with the issue. Available for issues which support escalation. |
 | <a id="epicissueescalationstatus"></a>`escalationStatus` | [`IssueEscalationStatus`](#issueescalationstatus) | Escalation status of the issue. |
+| <a id="epicissuehasepic"></a>`hasEpic` | [`Boolean!`](#boolean) | Indicates if the issue belongs to an epic. Can return true and not show an associated epic when the user has no access to the epic. |
 | <a id="epicissuehealthstatus"></a>`healthStatus` | [`HealthStatus`](#healthstatus) | Current health status. |
 | <a id="epicissuehidden"></a>`hidden` | [`Boolean`](#boolean) | Indicates the issue is hidden because the author has been banned. Will always return `null` if `ban_user_feature_flag` feature flag is disabled. |
 | <a id="epicissuehumantimeestimate"></a>`humanTimeEstimate` | [`String`](#string) | Human-readable time estimate of the issue. |
@@ -13332,6 +13333,7 @@ Describes an issuable resource link for incident issues.
 | <a id="issueepic"></a>`epic` | [`Epic`](#epic) | Epic to which this issue belongs. |
 | <a id="issueescalationpolicy"></a>`escalationPolicy` | [`EscalationPolicyType`](#escalationpolicytype) | Escalation policy associated with the issue. Available for issues which support escalation. |
 | <a id="issueescalationstatus"></a>`escalationStatus` | [`IssueEscalationStatus`](#issueescalationstatus) | Escalation status of the issue. |
+| <a id="issuehasepic"></a>`hasEpic` | [`Boolean!`](#boolean) | Indicates if the issue belongs to an epic. Can return true and not show an associated epic when the user has no access to the epic. |
 | <a id="issuehealthstatus"></a>`healthStatus` | [`HealthStatus`](#healthstatus) | Current health status. |
 | <a id="issuehidden"></a>`hidden` | [`Boolean`](#boolean) | Indicates the issue is hidden because the author has been banned. Will always return `null` if `ban_user_feature_flag` feature flag is disabled. |
 | <a id="issuehumantimeestimate"></a>`humanTimeEstimate` | [`String`](#string) | Human-readable time estimate of the issue. |
diff --git a/ee/app/graphql/ee/types/issue_type.rb b/ee/app/graphql/ee/types/issue_type.rb
index 6dd1967b12f82f2366d9618f50fd4eeeb4d2b923..d5738edad13517b7463ff7fb74b64ebc6e4eec62 100644
--- a/ee/app/graphql/ee/types/issue_type.rb
+++ b/ee/app/graphql/ee/types/issue_type.rb
@@ -8,6 +8,12 @@ module IssueType
       prepended do
         field :epic, ::Types::EpicType, null: true, description: 'Epic to which this issue belongs.'
 
+        field :has_epic, GraphQL::Types::Boolean,
+          null: false,
+          description: "Indicates if the issue belongs to an epic.
+            Can return true and not show an associated epic when the user has no access to the epic.",
+          method: :has_epic?
+
         field :iteration, ::Types::IterationType, null: true, description: 'Iteration of the issue.'
 
         field :weight, GraphQL::Types::Int, null: true, description: 'Weight of the issue.'
diff --git a/ee/app/models/ee/issue.rb b/ee/app/models/ee/issue.rb
index 8e1bd31ca3434e42184f19881d7c9218ce461f33..5419fb3145afc510704a26d129503ba5fe70c4a3 100644
--- a/ee/app/models/ee/issue.rb
+++ b/ee/app/models/ee/issue.rb
@@ -318,6 +318,10 @@ def update_upvotes_count
       super
     end
 
+    def has_epic?
+      epic_issue.present?
+    end
+
     private
 
     def blocking_issues_ids
diff --git a/ee/spec/graphql/types/issue_type_spec.rb b/ee/spec/graphql/types/issue_type_spec.rb
index b5a41ddf246b9d342f7f3d538a700e9511cb5b5b..5ef27564ff7bfa52f477d86b5659862925e634d3 100644
--- a/ee/spec/graphql/types/issue_type_spec.rb
+++ b/ee/spec/graphql/types/issue_type_spec.rb
@@ -4,6 +4,7 @@
 
 RSpec.describe GitlabSchema.types['Issue'] do
   it { expect(described_class).to have_graphql_field(:epic) }
+  it { expect(described_class).to have_graphql_field(:has_epic) }
   it { expect(described_class).to have_graphql_field(:iteration) }
   it { expect(described_class).to have_graphql_field(:weight) }
   it { expect(described_class).to have_graphql_field(:health_status) }
diff --git a/ee/spec/models/issue_spec.rb b/ee/spec/models/issue_spec.rb
index 71ada60898a91574a333d402240b45f47b07c4b8..226508135944306c62e704a745c3df5310f87a4f 100644
--- a/ee/spec/models/issue_spec.rb
+++ b/ee/spec/models/issue_spec.rb
@@ -1258,4 +1258,22 @@
       end
     end
   end
+
+  describe "#has_epic?" do
+    let(:issue) { build(:issue, epic: epic) }
+
+    subject(:has_epic) { issue.has_epic? }
+
+    context 'when when there is no associated epic' do
+      let(:epic) { nil }
+
+      it { is_expected.to eq false }
+    end
+
+    context 'when when there is an associated epic' do
+      let(:epic) { build(:epic) }
+
+      it { is_expected.to eq true }
+    end
+  end
 end
diff --git a/ee/spec/requests/api/graphql/issue/issue_spec.rb b/ee/spec/requests/api/graphql/issue/issue_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..26dc495f1212cc90e4b67d77bbd32cca956e481e
--- /dev/null
+++ b/ee/spec/requests/api/graphql/issue/issue_spec.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Query.issue(id)' do
+  include GraphqlHelpers
+
+  let_it_be(:group) { create(:group) }
+  let_it_be(:project) { create(:project, group: group) }
+  let_it_be(:epic) { create(:epic, :confidential, group: group) }
+
+  let(:issue) { create(:issue, :confidential, project: project, epic: epic) }
+  let(:current_user) { create(:user) }
+  let(:issue_params) { { 'id' => global_id_of(issue) } }
+  let(:issue_data) { graphql_data['issue'] }
+  let(:issue_fields) { ['hasEpic', 'epic { id }'] }
+
+  let(:query) do
+    graphql_query_for('issue', issue_params, issue_fields)
+  end
+
+  before do
+    stub_licensed_features(epics: true)
+  end
+
+  context 'when user has no access to the epic' do
+    before do
+      project.add_developer(current_user)
+    end
+
+    context 'when there is an epic' do
+      it 'returns null for epic and hasEpic is `true`' do
+        post_graphql(query, current_user: current_user)
+
+        expect(issue_data['hasEpic']).to eq(true)
+        expect(issue_data['epic']).to be_nil
+      end
+    end
+
+    context 'when there is no epic' do
+      let_it_be(:epic) { nil }
+
+      it 'returns null for epic and hasEpic is `false`' do
+        post_graphql(query, current_user: current_user)
+
+        expect(issue_data['hasEpic']).to eq(false)
+        expect(issue_data['epic']).to be_nil
+      end
+    end
+  end
+
+  context 'when user has access to the epic' do
+    before do
+      group.add_developer(current_user)
+    end
+
+    it 'returns epic and hasEpic is `true`' do
+      post_graphql(query, current_user: current_user)
+
+      expect(issue_data['hasEpic']).to eq(true)
+      expect(issue_data['epic']).to be_present
+    end
+  end
+end