diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md
index 08b69b1d7df4f063e1992b3c27f7764df785fe86..907e305b11b107621cdd157f81030ce6f467539a 100644
--- a/doc/api/graphql/reference/_index.md
+++ b/doc/api/graphql/reference/_index.md
@@ -828,6 +828,7 @@ four standard [pagination arguments](#pagination-arguments):
 | <a id="queryissuescreatedbefore"></a>`createdBefore` | [`Time`](#time) | Issues created before the date. |
 | <a id="queryissuescrmcontactid"></a>`crmContactId` | [`String`](#string) | ID of a contact assigned to the issues. |
 | <a id="queryissuescrmorganizationid"></a>`crmOrganizationId` | [`String`](#string) | ID of an organization assigned to the issues. |
+| <a id="queryissuescustomfield"></a>`customField` {{< icon name="warning-solid" >}} | [`[WorkItemWidgetCustomFieldFilterInputType!]`](#workitemwidgetcustomfieldfilterinputtype) | **Introduced** in GitLab 17.10. **Status**: Experiment. Filter by custom fields. |
 | <a id="queryissuesdueafter"></a>`dueAfter` | [`Time`](#time) | Return issues due on or after the given time. |
 | <a id="queryissuesduebefore"></a>`dueBefore` | [`Time`](#time) | Return issues due on or before the given time. |
 | <a id="queryissuesepicid"></a>`epicId` | [`String`](#string) | ID of an epic associated with the issues, "none" and "any" values are supported. |
@@ -27195,6 +27196,7 @@ four standard [pagination arguments](#pagination-arguments):
 | <a id="groupissuescreatedbefore"></a>`createdBefore` | [`Time`](#time) | Issues created before the date. |
 | <a id="groupissuescrmcontactid"></a>`crmContactId` | [`String`](#string) | ID of a contact assigned to the issues. |
 | <a id="groupissuescrmorganizationid"></a>`crmOrganizationId` | [`String`](#string) | ID of an organization assigned to the issues. |
+| <a id="groupissuescustomfield"></a>`customField` {{< icon name="warning-solid" >}} | [`[WorkItemWidgetCustomFieldFilterInputType!]`](#workitemwidgetcustomfieldfilterinputtype) | **Introduced** in GitLab 17.10. **Status**: Experiment. Filter by custom fields. |
 | <a id="groupissuesdueafter"></a>`dueAfter` | [`Time`](#time) | Return issues due on or after the given time. |
 | <a id="groupissuesduebefore"></a>`dueBefore` | [`Time`](#time) | Return issues due on or before the given time. |
 | <a id="groupissuesepicid"></a>`epicId` | [`String`](#string) | ID of an epic associated with the issues, "none" and "any" values are supported. |
@@ -34485,6 +34487,7 @@ Returns [`Issue`](#issue).
 | <a id="projectissuecreatedbefore"></a>`createdBefore` | [`Time`](#time) | Issues created before the date. |
 | <a id="projectissuecrmcontactid"></a>`crmContactId` | [`String`](#string) | ID of a contact assigned to the issues. |
 | <a id="projectissuecrmorganizationid"></a>`crmOrganizationId` | [`String`](#string) | ID of an organization assigned to the issues. |
+| <a id="projectissuecustomfield"></a>`customField` {{< icon name="warning-solid" >}} | [`[WorkItemWidgetCustomFieldFilterInputType!]`](#workitemwidgetcustomfieldfilterinputtype) | **Introduced** in GitLab 17.10. **Status**: Experiment. Filter by custom fields. |
 | <a id="projectissuedueafter"></a>`dueAfter` | [`Time`](#time) | Return issues due on or after the given time. |
 | <a id="projectissueduebefore"></a>`dueBefore` | [`Time`](#time) | Return issues due on or before the given time. |
 | <a id="projectissueepicid"></a>`epicId` | [`String`](#string) | ID of an epic associated with the issues, "none" and "any" values are supported. |
@@ -34539,6 +34542,7 @@ Returns [`IssueStatusCountsType`](#issuestatuscountstype).
 | <a id="projectissuestatuscountscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Issues created before the date. |
 | <a id="projectissuestatuscountscrmcontactid"></a>`crmContactId` | [`String`](#string) | ID of a contact assigned to the issues. |
 | <a id="projectissuestatuscountscrmorganizationid"></a>`crmOrganizationId` | [`String`](#string) | ID of an organization assigned to the issues. |
+| <a id="projectissuestatuscountscustomfield"></a>`customField` {{< icon name="warning-solid" >}} | [`[WorkItemWidgetCustomFieldFilterInputType!]`](#workitemwidgetcustomfieldfilterinputtype) | **Introduced** in GitLab 17.10. **Status**: Experiment. Filter by custom fields. |
 | <a id="projectissuestatuscountsdueafter"></a>`dueAfter` | [`Time`](#time) | Return issues due on or after the given time. |
 | <a id="projectissuestatuscountsduebefore"></a>`dueBefore` | [`Time`](#time) | Return issues due on or before the given time. |
 | <a id="projectissuestatuscountsepicid"></a>`epicId` | [`String`](#string) | ID of an epic associated with the issues, "none" and "any" values are supported. |
@@ -34594,6 +34598,7 @@ four standard [pagination arguments](#pagination-arguments):
 | <a id="projectissuescreatedbefore"></a>`createdBefore` | [`Time`](#time) | Issues created before the date. |
 | <a id="projectissuescrmcontactid"></a>`crmContactId` | [`String`](#string) | ID of a contact assigned to the issues. |
 | <a id="projectissuescrmorganizationid"></a>`crmOrganizationId` | [`String`](#string) | ID of an organization assigned to the issues. |
+| <a id="projectissuescustomfield"></a>`customField` {{< icon name="warning-solid" >}} | [`[WorkItemWidgetCustomFieldFilterInputType!]`](#workitemwidgetcustomfieldfilterinputtype) | **Introduced** in GitLab 17.10. **Status**: Experiment. Filter by custom fields. |
 | <a id="projectissuesdueafter"></a>`dueAfter` | [`Time`](#time) | Return issues due on or after the given time. |
 | <a id="projectissuesduebefore"></a>`dueBefore` | [`Time`](#time) | Return issues due on or before the given time. |
 | <a id="projectissuesepicid"></a>`epicId` | [`String`](#string) | ID of an epic associated with the issues, "none" and "any" values are supported. |
@@ -47741,6 +47746,15 @@ Attributes for value stream stage.
 | <a id="workitemwidgetcurrentusertodosinputaction"></a>`action` | [`WorkItemTodoUpdateAction!`](#workitemtodoupdateaction) | Action for the update. |
 | <a id="workitemwidgetcurrentusertodosinputtodoid"></a>`todoId` | [`TodoID`](#todoid) | Global ID of the to-do. If not present, all to-dos of the work item will be updated. |
 
+### `WorkItemWidgetCustomFieldFilterInputType`
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="workitemwidgetcustomfieldfilterinputtypecustomfieldid"></a>`customFieldId` | [`IssuablesCustomFieldID!`](#issuablescustomfieldid) | Global ID of the custom field. |
+| <a id="workitemwidgetcustomfieldfilterinputtypeselectedoptionids"></a>`selectedOptionIds` | [`[IssuablesCustomFieldSelectOptionID!]`](#issuablescustomfieldselectoptionid) | Global IDs of the selected options for custom fields with select type. |
+
 ### `WorkItemWidgetCustomFieldValueInputType`
 
 #### Arguments
diff --git a/ee/app/graphql/ee/resolvers/issues/base_resolver.rb b/ee/app/graphql/ee/resolvers/issues/base_resolver.rb
index 4bd5e8b4de041987032f343d40168bfbda5e8c57..95a2b5e8d4f9ca5b9601c50157118d709904d243 100644
--- a/ee/app/graphql/ee/resolvers/issues/base_resolver.rb
+++ b/ee/app/graphql/ee/resolvers/issues/base_resolver.rb
@@ -8,6 +8,11 @@ module BaseResolver
         extend ::Gitlab::Utils::Override
 
         prepended do
+          argument :custom_field, [::Types::WorkItems::Widgets::CustomFieldFilterInputType],
+            required: false,
+            experiment: { milestone: '17.10' },
+            description: 'Filter by custom fields.',
+            prepare: ->(custom_fields, _ctx) { Array(custom_fields).inject({}, :merge) }
           argument :epic_id, GraphQL::Types::String,
             required: false,
             description: 'ID of an epic associated with the issues, "none" and "any" values are supported.'
diff --git a/ee/app/graphql/types/work_items/widgets/custom_field_filter_input_type.rb b/ee/app/graphql/types/work_items/widgets/custom_field_filter_input_type.rb
new file mode 100644
index 0000000000000000000000000000000000000000..705bbf965d8d691ed4c93eb34b78b4ca4ca8c846
--- /dev/null
+++ b/ee/app/graphql/types/work_items/widgets/custom_field_filter_input_type.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Types
+  module WorkItems
+    module Widgets
+      class CustomFieldFilterInputType < BaseInputObject
+        graphql_name 'WorkItemWidgetCustomFieldFilterInputType'
+
+        argument :custom_field_id, ::Types::GlobalIDType[::Issuables::CustomField],
+          required: true,
+          description: copy_field_description(Types::Issuables::CustomFieldType, :id),
+          prepare: ->(id, _ctx) { id&.model_id }
+
+        argument :selected_option_ids, [::Types::GlobalIDType[::Issuables::CustomFieldSelectOption]],
+          required: false,
+          description: 'Global IDs of the selected options for custom fields with select type.',
+          prepare: ->(ids, _ctx) { ids.map(&:model_id) }
+
+        def prepare
+          { custom_field_id => selected_option_ids }
+        end
+      end
+    end
+  end
+end
diff --git a/ee/spec/requests/api/graphql/project/issues_spec.rb b/ee/spec/requests/api/graphql/project/issues_spec.rb
index 452fa5bd724b99886fcaf048fb12ddaf9a7e6d68..807209d0969ff422ec257db9399897e0804f5199 100644
--- a/ee/spec/requests/api/graphql/project/issues_spec.rb
+++ b/ee/spec/requests/api/graphql/project/issues_spec.rb
@@ -74,5 +74,48 @@ def query(params = issue_filter_params)
         expect(issues.first["id"]).to eq(issue_needs_attention.to_global_id.to_s)
       end
     end
+
+    context "for custom field" do
+      include_context 'with group configured with custom fields'
+
+      before_all do
+        create(:work_item_select_field_value, work_item_id: issue_a.id, custom_field: select_field,
+          custom_field_select_option: select_option_1)
+        create(:work_item_select_field_value, work_item_id: issue_b.id, custom_field: select_field,
+          custom_field_select_option: select_option_2)
+        create(:work_item_select_field_value, work_item_id: issue_c.id, custom_field: select_field,
+          custom_field_select_option: select_option_2)
+      end
+
+      before do
+        stub_licensed_features(custom_fields: true)
+      end
+
+      let(:query) do
+        graphql_query_for(:project, { full_path: project.full_path },
+          query_nodes(:issues, :id, args: params)
+        )
+      end
+
+      context "when filtering on a select field" do
+        let(:params) do
+          {
+            custom_field: [{
+              custom_field_id: select_field.to_global_id.to_s,
+              selected_option_ids: [select_option_2.to_global_id.to_s]
+            }]
+          }
+        end
+
+        it 'returns issues that match the custom field filter' do
+          post_graphql(query, current_user: current_user)
+
+          issues = graphql_data.dig('project', 'issues', 'nodes')
+
+          expect(issues.size).to eq(2)
+          expect(issues.pluck("id")).to contain_exactly(issue_b.to_global_id.to_s, issue_c.to_global_id.to_s)
+        end
+      end
+    end
   end
 end
diff --git a/spec/graphql/types/query_type_spec.rb b/spec/graphql/types/query_type_spec.rb
index deeed58fe0b5821645a3e86d0c3faaca66f520ab..f506c01e1fbfa8b0cd5eb7532781941c3ac645ee 100644
--- a/spec/graphql/types/query_type_spec.rb
+++ b/spec/graphql/types/query_type_spec.rb
@@ -245,6 +245,7 @@
           iterationWildcardId
           weight
           weightWildcardId
+          customField
         ]
       end