diff --git a/app/assets/javascripts/graphql_shared/queries/project_topics_search.query.graphql b/app/assets/javascripts/graphql_shared/queries/project_topics_search.query.graphql
index 0c0a874d950812f09cb803ece1c23915b85aa19f..cfcfce8d6fc7834ff557cb2a9b69f8b7a1cb5247 100644
--- a/app/assets/javascripts/graphql_shared/queries/project_topics_search.query.graphql
+++ b/app/assets/javascripts/graphql_shared/queries/project_topics_search.query.graphql
@@ -1,5 +1,5 @@
-query searchProjectTopics($search: String) {
-  topics(search: $search) {
+query searchProjectTopics($search: String, $organizationId: OrganizationsOrganizationID) {
+  topics(search: $search, organizationId: $organizationId) {
     nodes {
       id
       name
diff --git a/app/assets/javascripts/projects/settings/topics/components/topics_token_selector.vue b/app/assets/javascripts/projects/settings/topics/components/topics_token_selector.vue
index 15c43435e01d2729ded8c874e7ea1ff6bc914460..92dd80bb9c6931f8cdedd9ba177270c70727498f 100644
--- a/app/assets/javascripts/projects/settings/topics/components/topics_token_selector.vue
+++ b/app/assets/javascripts/projects/settings/topics/components/topics_token_selector.vue
@@ -4,6 +4,8 @@ import { helpPagePath } from '~/helpers/help_page_helper';
 import { s__ } from '~/locale';
 import { AVATAR_SHAPE_OPTION_RECT } from '~/vue_shared/constants';
 import searchProjectTopics from '~/graphql_shared/queries/project_topics_search.query.graphql';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
+import { TYPE_ORGANIZATION } from '~/graphql_shared/constants';
 
 export default {
   components: {
@@ -26,6 +28,10 @@ export default {
       required: false,
       default: () => [],
     },
+    organizationId: {
+      type: String,
+      required: true,
+    },
   },
   apollo: {
     topics: {
@@ -33,6 +39,7 @@ export default {
       variables() {
         return {
           search: this.search,
+          organizationId: convertToGraphQLId(TYPE_ORGANIZATION, this.organizationId),
         };
       },
       update(data) {
diff --git a/app/assets/javascripts/projects/settings/topics/index.js b/app/assets/javascripts/projects/settings/topics/index.js
index 3fbd1a61abedb8c95888f08ba9bc5dea2249e141..8819b046d99be809d042e8b077406356e6d7a206 100644
--- a/app/assets/javascripts/projects/settings/topics/index.js
+++ b/app/assets/javascripts/projects/settings/topics/index.js
@@ -14,7 +14,7 @@ export default () => {
 
   if (!el) return null;
 
-  const { hiddenInputId } = el.dataset;
+  const { hiddenInputId, organizationId } = el.dataset;
   const hiddenInput = document.getElementById(hiddenInputId);
 
   const selected = hiddenInput.value
@@ -31,6 +31,7 @@ export default () => {
       return createElement(TopicsTokenSelector, {
         props: {
           selected,
+          organizationId,
         },
         on: {
           update(tokens) {
diff --git a/app/graphql/resolvers/topics_resolver.rb b/app/graphql/resolvers/topics_resolver.rb
index 4aadc9485240f335186e253fcce5dd6e8318d87f..844a73f9a6c574e24c1bed40953222efe8f771ff 100644
--- a/app/graphql/resolvers/topics_resolver.rb
+++ b/app/graphql/resolvers/topics_resolver.rb
@@ -2,18 +2,40 @@
 
 module Resolvers
   class TopicsResolver < BaseResolver
+    include Gitlab::Graphql::Authorize::AuthorizeResource
+
     type Types::Projects::TopicType, null: true
 
     argument :search, GraphQL::Types::String,
       required: false,
       description: 'Search query for topic name.'
 
+    argument :organization_id, Types::GlobalIDType[::Organizations::Organization],
+      required: false,
+      prepare: ->(global_id, _ctx) { global_id&.model_id },
+      experiment: { milestone: '17.7' },
+      description: 'Global ID of the organization.'
+
     def resolve(**args)
-      if args[:search].present?
-        ::Projects::Topic.search(args[:search]).order_by_non_private_projects_count
-      else
-        ::Projects::Topic.order_by_non_private_projects_count
-      end
+      organization = authorized_find!(id: args[:organization_id] || ::Current.organization_id)
+
+      return organization_topics(organization.id) unless args[:search].present?
+
+      organization_topics(organization.id).search(args[:search])
+    end
+
+    private
+
+    def find_object(id:)
+      ::Organizations::Organization.find_by_id(id)
+    end
+
+    def authorized_resource?(organization)
+      Ability.allowed?(current_user, :read_organization, organization)
+    end
+
+    def organization_topics(organization_id)
+      ::Projects::Topic.for_organization(organization_id).order_by_non_private_projects_count
     end
   end
 end
diff --git a/app/views/projects/settings/_general.html.haml b/app/views/projects/settings/_general.html.haml
index a57bbf236e7818511bc55bbd4e1d8b83de63e035..6f5453281ee20688b83ce0cf0f3c72afd8e31232 100644
--- a/app/views/projects/settings/_general.html.haml
+++ b/app/views/projects/settings/_general.html.haml
@@ -36,7 +36,7 @@
 
     .row
       .form-group.col-md-9
-        .js-topics-selector{ data: { hidden_input_id: hidden_topics_field_id } }
+        .js-topics-selector{ data: { hidden_input_id: hidden_topics_field_id, organization_id: @project.organization.id } }
         = f.hidden_field :topics, value: @project.topic_list.join(', '), id: hidden_topics_field_id
 
   = f.submit _('Save changes'), pajamas_button: true, data: { testid: 'save-naming-topics-avatar-button' }
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 3f34169b1d11ed892e42fae291f5176340c8a662..309045cd6e73d23677491903d41af77c33aa2196 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -1273,6 +1273,7 @@ four standard [pagination arguments](#pagination-arguments):
 
 | Name | Type | Description |
 | ---- | ---- | ----------- |
+| <a id="querytopicsorganizationid"></a>`organizationId` **{warning-solid}** | [`OrganizationsOrganizationID`](#organizationsorganizationid) | **Introduced** in GitLab 17.7. **Status**: Experiment. Global ID of the organization. |
 | <a id="querytopicssearch"></a>`search` | [`String`](#string) | Search query for topic name. |
 
 ### `Query.usageTrendsMeasurements`
diff --git a/ee/spec/views/projects/edit.html.haml_spec.rb b/ee/spec/views/projects/edit.html.haml_spec.rb
index fd189086a8f0361c16f10838e70777b6dc5ffb3d..b1ba8c16a85bf10e52633b17a56c9bc8e767cfbb 100644
--- a/ee/spec/views/projects/edit.html.haml_spec.rb
+++ b/ee/spec/views/projects/edit.html.haml_spec.rb
@@ -3,7 +3,8 @@
 require 'spec_helper'
 
 RSpec.describe 'projects/edit' do
-  let(:project) { create(:project) }
+  let_it_be(:organization) { create(:organization) }
+  let(:project) { create(:project, organization: organization) }
   let(:user) { create(:admin) }
 
   before do
@@ -94,7 +95,9 @@
       end
 
       context 'when project is pending deletion' do
-        let_it_be(:project) { build_stubbed(:project, marked_for_deletion_at: Date.current) }
+        let_it_be(:project) do
+          build_stubbed(:project, marked_for_deletion_at: Date.current, organization: organization)
+        end
 
         it_behaves_like 'renders restore project settings'
       end
@@ -110,7 +113,9 @@
       end
 
       context 'when project is pending deletion' do
-        let_it_be(:project) { build_stubbed(:project, marked_for_deletion_at: Date.current) }
+        let_it_be(:project) do
+          build_stubbed(:project, marked_for_deletion_at: Date.current, organization: organization)
+        end
 
         it_behaves_like 'does not render restore project settings'
       end
diff --git a/spec/graphql/resolvers/topics_resolver_spec.rb b/spec/graphql/resolvers/topics_resolver_spec.rb
index 89f4583bce8882d728386773c2c08de435e58e73..de42c5b95e31726a3b5d67d02bfa78264ade70da 100644
--- a/spec/graphql/resolvers/topics_resolver_spec.rb
+++ b/spec/graphql/resolvers/topics_resolver_spec.rb
@@ -6,28 +6,92 @@
   include GraphqlHelpers
 
   describe '#resolve' do
-    let!(:topic1) { create(:topic, name: 'GitLab', non_private_projects_count: 1) }
-    let!(:topic2) { create(:topic, name: 'git', non_private_projects_count: 2) }
-    let!(:topic3) { create(:topic, name: 'topic3', non_private_projects_count: 3) }
+    let_it_be(:organization) { create(:organization) }
+    let(:organization_id) { organization.to_global_id }
 
-    it 'finds all topics' do
-      expect(resolve_topics).to eq([topic3, topic2, topic1])
+    let(:topic1) do
+      create(:topic, name: 'GitLab', non_private_projects_count: 1, organization: organization)
     end
 
-    context 'with search' do
-      it 'searches environment by name' do
-        expect(resolve_topics(search: 'git')).to eq([topic2, topic1])
+    let(:topic2) do
+      create(:topic, name: 'git', non_private_projects_count: 2, organization: organization)
+    end
+
+    let(:topic3) do
+      create(:topic, name: 'topic3', non_private_projects_count: 3, organization: organization)
+    end
+
+    shared_examples 'topics query' do
+      it 'finds all topics' do
+        expect(resolve_topics).to eq([topic3, topic2, topic1])
       end
 
-      context 'when the search term does not match any topic' do
-        it 'is empty' do
-          expect(resolve_topics(search: 'nonsense')).to be_empty
+      context 'with search' do
+        it 'searches environment by name' do
+          expect(resolve_topics(search: 'git')).to eq([topic2, topic1])
+        end
+
+        context 'when the search term does not match any topic' do
+          it 'is empty' do
+            expect(resolve_topics(search: 'nonsense')).to be_empty
+          end
+        end
+      end
+
+      context 'with organization id' do
+        it 'finds all topics' do
+          expect(resolve_topics(organization_id: organization_id)).to eq([topic3, topic2, topic1])
+        end
+
+        it 'matches searched organization topics' do
+          expect(resolve_topics(organization_id: organization_id, search: 'topic')).to eq([topic3])
+        end
+      end
+    end
+
+    shared_examples 'resource not available' do
+      it 'raises a GraphQL exception' do
+        expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ResourceNotAvailable) do
+          resolve_topics(organization_id: organization_id)
         end
       end
     end
+
+    context 'when no current user is set' do
+      let_it_be(:organization) { create(:organization, :public) }
+      let(:user) { nil }
+
+      it_behaves_like 'topics query'
+    end
+
+    context 'when no current user is set having no public organization' do
+      let(:user) { nil }
+
+      before do
+        Organizations::Organization.update_all(visibility_level: Organizations::Organization::INTERNAL)
+      end
+
+      it_behaves_like 'resource not available'
+    end
+
+    context 'when current user is set' do
+      let_it_be(:user) { create(:user, organizations: [organization]) }
+
+      it_behaves_like 'topics query'
+    end
+
+    context 'when current user is not a member of the organization' do
+      let_it_be(:user) { create(:user) }
+      let_it_be(:private_organization) { create(:organization, :private) }
+
+      let(:organization_id) { private_organization.to_global_id }
+
+      it_behaves_like 'resource not available'
+    end
   end
 
   def resolve_topics(args = {})
-    resolve(described_class, args: args)
+    args[:organization_id] = organization.to_global_id unless args[:organization_id]
+    resolve(described_class, args: args, ctx: { current_user: user })
   end
 end