diff --git a/ee/app/assets/javascripts/roles_and_permissions/components/create_member_role.vue b/ee/app/assets/javascripts/roles_and_permissions/components/create_member_role.vue
index 7823c6e00f35a4f25ce59131c57ced0e48d10452..06498cda889459c9f3c2c2f54b54c0e6ea5d8628 100644
--- a/ee/app/assets/javascripts/roles_and_permissions/components/create_member_role.vue
+++ b/ee/app/assets/javascripts/roles_and_permissions/components/create_member_role.vue
@@ -277,7 +277,7 @@ export default {
         <div class="gl-mb-3">
           <gl-sprintf :message="$options.i18n.baseRoleHelpText">
             <template #link="{ content }">
-              <gl-link :href="staticRolesHelpPagePath">{{ content }}</gl-link>
+              <gl-link :href="staticRolesHelpPagePath" target="_blank">{{ content }}</gl-link>
             </template>
           </gl-sprintf>
         </div>
@@ -292,7 +292,7 @@ export default {
       />
     </gl-form-group>
 
-    <permissions-selector :permissions.sync="memberRole.permissions" :state="isPermissionsValid" />
+    <permissions-selector v-model="memberRole.permissions" :is-valid="isPermissionsValid" />
 
     <div class="gl-display-flex gl-flex-wrap gl-gap-3">
       <gl-button
diff --git a/ee/app/assets/javascripts/roles_and_permissions/components/permissions_selector.vue b/ee/app/assets/javascripts/roles_and_permissions/components/permissions_selector.vue
index db6c246b5c8d91c620d45dee72b24763cc3ca362..bf838e33fb97220137e43d33aaa7e65cf02ccd0e 100644
--- a/ee/app/assets/javascripts/roles_and_permissions/components/permissions_selector.vue
+++ b/ee/app/assets/javascripts/roles_and_permissions/components/permissions_selector.vue
@@ -1,30 +1,44 @@
 <script>
-import { GlFormCheckbox, GlFormCheckboxGroup, GlFormGroup, GlSkeletonLoader } from '@gitlab/ui';
-import { difference, pull } from 'lodash';
-import { createAlert } from '~/alert';
+import { GlFormCheckbox, GlLoadingIcon, GlTable, GlSprintf, GlLink, GlAlert } from '@gitlab/ui';
+import { pull } from 'lodash';
 import { s__ } from '~/locale';
 import memberRolePermissionsQuery from 'ee/roles_and_permissions/graphql/member_role_permissions.query.graphql';
+import { helpPagePath } from '~/helpers/help_page_helper';
+
+export const FIELDS = [
+  { key: 'checkbox', label: '' },
+  { key: 'name', label: s__('MemberRole|Permission') },
+  { key: 'description', label: s__('MemberRole|Description') },
+];
 
 export default {
   i18n: {
     customPermissionsLabel: s__('MemberRole|Custom permissions'),
     customPermissionsDescription: s__(
-      'MemberRole|Add at least one custom permission to the base role.',
+      'MemberRole|Learn more about %{linkStart}available custom permissions%{linkEnd}.',
     ),
     permissionsFetchError: s__('MemberRole|Could not fetch available permissions.'),
+    permissionsSelected: s__('MemberRole|%{count} of %{total} permissions selected'),
+    permissionsSelectionError: s__('MemberRole|Select at least one permission.'),
   },
   components: {
-    GlFormCheckboxGroup,
     GlFormCheckbox,
-    GlFormGroup,
-    GlSkeletonLoader,
+    GlLoadingIcon,
+    GlLink,
+    GlSprintf,
+    GlTable,
+    GlAlert,
+  },
+  model: {
+    prop: 'permissions',
+    event: 'change',
   },
   props: {
     permissions: {
       type: Array,
       required: true,
     },
-    state: {
+    isValid: {
       type: Boolean,
       required: true,
     },
@@ -41,16 +55,30 @@ export default {
         return data.memberRolePermissions?.nodes || [];
       },
       error() {
-        createAlert({ message: this.$options.i18n.permissionsFetchError });
+        this.availablePermissions = [];
       },
     },
   },
   computed: {
+    docsPath() {
+      return helpPagePath('user/custom_roles/abilities');
+    },
     isLoadingPermissions() {
       return this.$apollo.queries.availablePermissions.loading;
     },
-    permissionsState() {
-      return this.state ? null : false;
+    isErrorLoadingPermissions() {
+      return !this.isLoadingPermissions && !this.hasAvailablePermissions;
+    },
+    hasAvailablePermissions() {
+      return this.availablePermissions.length > 0;
+    },
+    isSomePermissionsSelected() {
+      return this.permissions.length > 0 && !this.isAllPermissionsSelected;
+    },
+    isAllPermissionsSelected() {
+      return (
+        !this.isLoadingPermissions && this.permissions.length >= this.availablePermissions.length
+      );
     },
     parentPermissionsLookup() {
       return this.availablePermissions.reduce((acc, { value, requirements }) => {
@@ -74,20 +102,37 @@ export default {
     },
   },
   methods: {
-    emitSelectedPermissions(selected) {
-      const added = difference(selected, this.permissions);
-      const removed = difference(this.permissions, selected);
-      // Check/uncheck any dependent permissions based on what permissions are selected.
-      added.forEach((permission) => this.selectParentPermissions(permission, selected));
-      removed.forEach((permission) => this.deselectChildPermissions(permission, selected));
+    isSelected({ value }) {
+      return this.permissions.includes(value);
+    },
+    updatePermissions({ value }) {
+      const selected = [...this.permissions];
+
+      if (selected.includes(value)) {
+        // Permission is being removed, remove it and deselect any child permissions.
+        pull(selected, value);
+        this.deselectChildPermissions(value, selected);
+      } else {
+        // Permission is being added, select it and select any parent permissions.
+        selected.push(value);
+        this.selectParentPermissions(value, selected);
+      }
 
-      this.$emit('update:permissions', selected);
+      this.emitPermissionsUpdate(selected);
+    },
+    toggleAllPermissions() {
+      const permissions = this.isAllPermissionsSelected ? [] : this.availablePermissions;
+      this.emitPermissionsUpdate(permissions.map(({ value }) => value));
+    },
+    emitPermissionsUpdate(permissions) {
+      this.$emit('change', permissions);
     },
     selectParentPermissions(permission, selected) {
       const parentPermissions = this.parentPermissionsLookup[permission];
 
       parentPermissions?.forEach((parent) => {
-        // Only select the parent permission if it's not already selected.
+        // Only select the parent permission if it's not selected. This prevents an infinite loop if there are
+        // circular dependencies, i.e. A depends on B and B depends on A.
         if (!selected.includes(parent)) {
           selected.push(parent);
           this.selectParentPermissions(parent, selected);
@@ -98,7 +143,8 @@ export default {
       const childPermissions = this.childPermissionsLookup[permission];
 
       childPermissions?.forEach((child) => {
-        // Only remove the child permission if it's selected.
+        // Only unselect the child permission if it's already selected. This prevents an infinite loop if there are
+        // circular dependencies, i.e. A depends on B and B depends on A.
         if (selected.includes(child)) {
           pull(selected, child);
           this.deselectChildPermissions(child, selected);
@@ -106,38 +152,78 @@ export default {
       });
     },
   },
+  FIELDS,
 };
 </script>
 
 <template>
-  <gl-form-group :label="$options.i18n.customPermissionsLabel" label-class="gl-pb-1!">
-    <template #label-description>
-      <div v-if="!isLoadingPermissions" class="gl-mb-6">
-        {{ $options.i18n.customPermissionsDescription }}
-      </div>
-    </template>
-
-    <div v-if="isLoadingPermissions" class="gl-mt-5">
-      <gl-skeleton-loader />
-    </div>
-
-    <gl-form-checkbox-group
-      v-else
-      :checked="permissions"
-      :state="permissionsState"
-      @input="emitSelectedPermissions"
-    >
-      <gl-form-checkbox
-        v-for="permission in availablePermissions"
-        :key="permission.value"
-        :value="permission.value"
-        :data-testid="permission.value"
+  <fieldset>
+    <legend class="gl-mb-1 gl-border-b-0 gl-font-base">
+      <span class="gl-font-weight-bold">{{ $options.i18n.customPermissionsLabel }}</span>
+      <span
+        v-if="hasAvailablePermissions"
+        class="gl-text-gray-400 gl-ml-3"
+        data-testid="permissions-selected-message"
       >
-        {{ permission.name }}
-        <template v-if="permission.description" #help>
-          {{ permission.description }}
+        <gl-sprintf :message="$options.i18n.permissionsSelected">
+          <template #count>{{ permissions.length }}</template>
+          <template #total>{{ availablePermissions.length }}</template>
+        </gl-sprintf>
+      </span>
+    </legend>
+
+    <p class="gl-mb-4">
+      <gl-sprintf :message="$options.i18n.customPermissionsDescription">
+        <template #link="{ content }">
+          <gl-link :href="docsPath" target="_blank">{{ content }}</gl-link>
         </template>
-      </gl-form-checkbox>
-    </gl-form-checkbox-group>
-  </gl-form-group>
+      </gl-sprintf>
+    </p>
+
+    <p v-if="!isValid" class="gl-text-red-500">{{ $options.i18n.permissionsSelectionError }}</p>
+
+    <gl-alert
+      v-if="isErrorLoadingPermissions"
+      :dismissible="false"
+      variant="danger"
+      class="gl-mb-6 gl-display-inline-block"
+    >
+      {{ $options.i18n.permissionsFetchError }}
+    </gl-alert>
+
+    <gl-table
+      v-else
+      :items="availablePermissions"
+      :fields="$options.FIELDS"
+      :busy="isLoadingPermissions"
+      hover
+      selected-variant=""
+      selectable
+      class="gl-my-8"
+      @row-clicked="updatePermissions"
+    >
+      <template #table-busy>
+        <gl-loading-icon size="md" />
+      </template>
+
+      <template #head(checkbox)>
+        <gl-form-checkbox
+          :disabled="isLoadingPermissions"
+          :checked="isAllPermissionsSelected"
+          :indeterminate="isSomePermissionsSelected"
+          @change="toggleAllPermissions"
+        />
+      </template>
+
+      <template #cell(checkbox)="{ item }">
+        <gl-form-checkbox :checked="isSelected(item)" @change="updatePermissions(item)" />
+      </template>
+
+      <template #cell(name)="{ item }">
+        <span :class="{ 'gl-text-red-500': !isValid }" class="gl-white-space-nowrap">
+          {{ item.name }}
+        </span>
+      </template>
+    </gl-table>
+  </fieldset>
 </template>
diff --git a/ee/spec/features/admin/member_roles_spec.rb b/ee/spec/features/admin/member_roles_spec.rb
index f1803f61df1ec7d0128fd3efe33725decdb10e44..e20063eb6e87d2e9ce765d192fd9d58afe7e3a35 100644
--- a/ee/spec/features/admin/member_roles_spec.rb
+++ b/ee/spec/features/admin/member_roles_spec.rb
@@ -22,8 +22,9 @@ def create_role(access_level, name, description, permissions)
     fill_in 'Name', with: name
     fill_in 'Description', with: description
     select access_level, from: 'Base role'
+
     permissions.each do |permission|
-      page.check permission
+      page.find('tr', text: permission).click
     end
 
     click_button s_('MemberRole|Create role')
diff --git a/ee/spec/features/groups/member_roles_spec.rb b/ee/spec/features/groups/member_roles_spec.rb
index b67df583d4e62f661c06a44acc921b98424d3d5b..29220e329cf2debb04470d7e5627215d8750c896 100644
--- a/ee/spec/features/groups/member_roles_spec.rb
+++ b/ee/spec/features/groups/member_roles_spec.rb
@@ -26,7 +26,7 @@ def create_role(access_level, name, description, permissions)
     select access_level, from: 'Base role'
 
     permissions.each do |permission|
-      page.check(permission)
+      page.find('tr', text: permission).click
     end
 
     click_button s_('MemberRole|Create role')
diff --git a/ee/spec/frontend/roles_and_permissions/components/create_member_role_spec.js b/ee/spec/frontend/roles_and_permissions/components/create_member_role_spec.js
index 117372e24c0e3bc0a1880f76cfe03b4857d8f015..48a41ba394f3c0c374c56cbdad7b31848baad77a 100644
--- a/ee/spec/frontend/roles_and_permissions/components/create_member_role_spec.js
+++ b/ee/spec/frontend/roles_and_permissions/components/create_member_role_spec.js
@@ -72,7 +72,7 @@ describe('CreateMemberRole', () => {
     findSelect().setValue('GUEST');
     findNameField().setValue('My role name');
     findDescriptionField().setValue('My description');
-    findPermissionsSelector().vm.$emit('update:permissions', ['A']);
+    findPermissionsSelector().vm.$emit('change', ['A']);
 
     return nextTick();
   };
@@ -143,12 +143,12 @@ describe('CreateMemberRole', () => {
     });
 
     it('shows a warning if no permissions are selected', async () => {
-      expect(findPermissionsSelector().props('state')).toBe(true);
+      expect(findPermissionsSelector().props('isValid')).toBe(true);
 
       findPermissionsSelector().vm.$emit('update:permissions', []);
       await submitForm();
 
-      expect(findPermissionsSelector().props('state')).toBe(false);
+      expect(findPermissionsSelector().props('isValid')).toBe(false);
     });
   });
 
diff --git a/ee/spec/frontend/roles_and_permissions/components/permissions_selector_spec.js b/ee/spec/frontend/roles_and_permissions/components/permissions_selector_spec.js
index 96206ce904d55f597424f03ec84e0bcc1e088676..d67223c53577bee40b2a2f23397ea5def065740a 100644
--- a/ee/spec/frontend/roles_and_permissions/components/permissions_selector_spec.js
+++ b/ee/spec/frontend/roles_and_permissions/components/permissions_selector_spec.js
@@ -1,85 +1,117 @@
-import { GlFormCheckbox, GlFormCheckboxGroup, GlSkeletonLoader } from '@gitlab/ui';
+import { GlTable, GlFormCheckbox, GlAlert, GlSprintf } from '@gitlab/ui';
 import Vue from 'vue';
 import VueApollo from 'vue-apollo';
-import { createAlert } from '~/alert';
 import createMockApollo from 'helpers/mock_apollo_helper';
-import PermissionsSelector from 'ee/roles_and_permissions/components/permissions_selector.vue';
-import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import PermissionsSelector, {
+  FIELDS,
+} from 'ee/roles_and_permissions/components/permissions_selector.vue';
+import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
+import { stubComponent } from 'helpers/stub_component';
 import waitForPromises from 'helpers/wait_for_promises';
 import memberRolePermissionsQuery from 'ee/roles_and_permissions/graphql/member_role_permissions.query.graphql';
 import { mockPermissions, mockDefaultPermissions } from '../mock_data';
 
 Vue.use(VueApollo);
-jest.mock('~/alert');
 
 describe('Permissions Selector component', () => {
   let wrapper;
 
   const defaultAvailablePermissionsHandler = jest.fn().mockResolvedValue(mockPermissions);
+  const glTableStub = stubComponent(GlTable, { props: ['items', 'fields', 'busy'] });
 
   const createComponent = ({
+    mountFn = shallowMountExtended,
     permissions = [],
-    state = true,
+    isValid = true,
     availablePermissionsHandler = defaultAvailablePermissionsHandler,
-    mountFn = shallowMountExtended,
   } = {}) => {
     wrapper = mountFn(PermissionsSelector, {
-      propsData: { permissions, state },
+      propsData: { permissions, isValid },
       apolloProvider: createMockApollo([[memberRolePermissionsQuery, availablePermissionsHandler]]),
+      stubs: {
+        GlSprintf,
+        ...(mountFn === shallowMountExtended ? { GlTable: glTableStub } : {}),
+      },
     });
 
     return waitForPromises();
   };
 
-  const findCheckboxes = () => wrapper.findAllComponents(GlFormCheckbox);
-  const findCheckboxGroup = () => wrapper.findComponent(GlFormCheckboxGroup);
-  const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
+  const findTable = () => wrapper.findComponent(GlTable);
+  const findPermissionsSelectedMessage = () => wrapper.findByTestId('permissions-selected-message');
+  const findAlert = () => wrapper.findComponent(GlAlert);
 
-  const checkPermissions = (permissions) => {
-    findCheckboxGroup().vm.$emit('input', permissions);
+  const checkPermission = (value) => {
+    findTable().vm.$emit('row-clicked', { value });
   };
 
-  const expectCheckedPermissions = (expected) => {
-    const permissions = wrapper.emitted('update:permissions')[0][0];
+  const expectSelectedPermissions = (expected) => {
+    const permissions = wrapper.emitted('change')[0][0];
 
     expect(permissions.sort()).toEqual(expected.sort());
   };
 
   describe('available permissions', () => {
-    it('loads the available permissions', () => {
-      createComponent();
+    describe('when loading', () => {
+      beforeEach(() => {
+        createComponent();
+      });
 
-      expect(defaultAvailablePermissionsHandler).toHaveBeenCalledTimes(1);
-    });
+      it('calls the query', () => {
+        expect(defaultAvailablePermissionsHandler).toHaveBeenCalledTimes(1);
+      });
+
+      it('shows the table as busy', () => {
+        expect(findTable().props('busy')).toBe(true);
+      });
 
-    it('shows the GlSkeletonLoader when the query is loading', () => {
-      createComponent();
+      it('does not show the permissions selected message', () => {
+        expect(findPermissionsSelectedMessage().exists()).toBe(false);
+      });
 
-      expect(findSkeletonLoader().exists()).toBe(true);
-      expect(findCheckboxes()).toHaveLength(0);
+      it('does not show the error message', () => {
+        expect(findAlert().exists()).toBe(false);
+      });
     });
 
-    it('shows the expected permissions when loaded', async () => {
-      await createComponent();
+    describe('after data is loaded', () => {
+      beforeEach(() => {
+        return createComponent();
+      });
 
-      expect(findSkeletonLoader().exists()).toBe(false);
-      expect(findCheckboxes()).toHaveLength(mockDefaultPermissions.length);
+      it('shows the table with the expected permissions', () => {
+        expect(findTable().props('busy')).toBe(false);
+        expect(findTable().props()).toMatchObject({
+          fields: FIELDS,
+          items: mockDefaultPermissions,
+        });
+      });
 
-      mockDefaultPermissions.forEach((permission, index) => {
-        const checkboxText = findCheckboxes().at(index).text();
+      it('shows the permissions selected message', () => {
+        expect(findPermissionsSelectedMessage().text()).toBe('0 of 7 permissions selected');
+      });
 
-        expect(checkboxText).toContain(permission.name);
-        expect(checkboxText).toContain(permission.description);
+      it('does not show the error message', () => {
+        expect(findAlert().exists()).toBe(false);
       });
     });
 
-    it('shows an error if the query fails', async () => {
-      const availablePermissionsHandler = jest.fn().mockRejectedValue(new Error('failed'));
-      await createComponent({ availablePermissionsHandler });
+    describe('on query error', () => {
+      beforeEach(() => {
+        const availablePermissionsHandler = jest.fn().mockRejectedValue();
+        return createComponent({ availablePermissionsHandler });
+      });
+
+      it('shows the error message', () => {
+        expect(findAlert().text()).toBe('Could not fetch available permissions.');
+      });
 
-      expect(findCheckboxes()).toHaveLength(0);
-      expect(createAlert).toHaveBeenCalledWith({
-        message: 'Could not fetch available permissions.',
+      it('does not show the table', () => {
+        expect(findTable().exists()).toBe(false);
+      });
+
+      it('does not show the permissions selected message', () => {
+        expect(findPermissionsSelectedMessage().exists()).toBe(false);
       });
     });
   });
@@ -96,9 +128,9 @@ describe('Permissions Selector component', () => {
       ${'G'}     | ${['A', 'B', 'C', 'G']}
     `('selects $expected when $permission is selected', async ({ permission, expected }) => {
       await createComponent();
-      checkPermissions([permission]);
+      checkPermission(permission);
 
-      expectCheckedPermissions(expected);
+      expectSelectedPermissions(expected);
     });
 
     it.each`
@@ -114,28 +146,34 @@ describe('Permissions Selector component', () => {
       'selects $expected when all permissions start off selected and $permission is unselected',
       async ({ permission, expected }) => {
         const permissions = mockDefaultPermissions.map((p) => p.value);
-        const selectedPermissions = permissions.filter((v) => v !== permission);
         await createComponent({ permissions });
         // Uncheck the permission by removing it from all permissions.
-        checkPermissions(selectedPermissions);
+        checkPermission(permission);
 
-        expectCheckedPermissions(expected);
+        expectSelectedPermissions(expected);
       },
     );
+
+    it('checks the permission when the table row is clicked', async () => {
+      await createComponent({ mountFn: mountExtended });
+      findTable().find('tbody tr').trigger('click');
+
+      expectSelectedPermissions(['A']);
+    });
+
+    it('checks the permission when the checkbox is clicked', async () => {
+      await createComponent({ mountFn: mountExtended });
+      wrapper.findAllComponents(GlFormCheckbox).at(1).trigger('click');
+
+      expectSelectedPermissions(['A']);
+    });
   });
 
   describe('validation state', () => {
-    it.each`
-      state    | expectedIsInvalid
-      ${true}  | ${false}
-      ${false} | ${true}
-    `('shows validation as $state when state is $state', async ({ state, expectedIsInvalid }) => {
-      await createComponent({ state, mountFn: mountExtended });
-
-      const input = findCheckboxes().at(0).find('input');
-      // is-valid should always be false, otherwise the text color will be green instead of black.
-      expect(input.classes('is-valid')).toBe(false);
-      expect(input.classes('is-invalid')).toBe(expectedIsInvalid);
+    it.each([true, false])('shows the expected text when isValid prop is %s', async (isValid) => {
+      await createComponent({ mountFn: mountExtended, isValid });
+
+      expect(wrapper.find('tbody td span').classes('gl-text-red-500')).toBe(!isValid);
     });
   });
 });
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index c13771d98028b7df81fd033b96e27dcb6a3f3367..1f47d556d4c448a423e27cf8b3dfda3a28c7e15f 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -31370,13 +31370,13 @@ msgstr ""
 msgid "MemberInviteEmail|Invitation to join the %{project_or_group} %{project_or_group_name}"
 msgstr ""
 
-msgid "MemberRole|%{requirement} has to be enabled in order to enable %{permission}"
+msgid "MemberRole|%{count} of %{total} permissions selected"
 msgstr ""
 
-msgid "MemberRole|Actions"
+msgid "MemberRole|%{requirement} has to be enabled in order to enable %{permission}"
 msgstr ""
 
-msgid "MemberRole|Add at least one custom permission to the base role."
+msgid "MemberRole|Actions"
 msgstr ""
 
 msgid "MemberRole|Are you sure you want to delete this custom role? Before you delete this custom role, make sure no group member has this role."
@@ -31454,6 +31454,9 @@ msgstr ""
 msgid "MemberRole|ID"
 msgstr ""
 
+msgid "MemberRole|Learn more about %{linkStart}available custom permissions%{linkEnd}."
+msgstr ""
+
 msgid "MemberRole|Manage roles"
 msgstr ""
 
@@ -31472,6 +31475,9 @@ msgstr ""
 msgid "MemberRole|No description"
 msgstr ""
 
+msgid "MemberRole|Permission"
+msgstr ""
+
 msgid "MemberRole|Permissions"
 msgstr ""
 
@@ -31490,6 +31496,9 @@ msgstr ""
 msgid "MemberRole|Select a %{linkStart}pre-existing static role%{linkEnd} to predefine a set of permissions."
 msgstr ""
 
+msgid "MemberRole|Select at least one permission."
+msgstr ""
+
 msgid "MemberRole|Standard roles"
 msgstr ""