diff --git a/docs/rules/require-explicit-emits.md b/docs/rules/require-explicit-emits.md
index f7ce60dd6..90cb03edb 100644
--- a/docs/rules/require-explicit-emits.md
+++ b/docs/rules/require-explicit-emits.md
@@ -112,6 +112,7 @@ export default {
 ## :couple: Related Rules
 
 - [vue/no-unused-emit-declarations](./no-unused-emit-declarations.md)
+- [vue/require-explicit-slots](./require-explicit-slots.md)
 
 ## :books: Further Reading
 
diff --git a/docs/rules/require-explicit-slots.md b/docs/rules/require-explicit-slots.md
new file mode 100644
index 000000000..ac3050896
--- /dev/null
+++ b/docs/rules/require-explicit-slots.md
@@ -0,0 +1,68 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/require-explicit-slots
+description: require slots to be explicitly defined with defineSlots
+---
+
+# vue/require-explicit-slots
+
+> require slots to be explicitly defined
+
+- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
+
+## :book: Rule Details
+
+This rule enforces all slots used in the template to be defined once either in the `script setup` block with the [`defineSlots`](https://vuejs.org/api/sfc-script-setup.html) macro, or with the [`slots property`](https://vuejs.org/api/options-rendering.html#slots) in the Options API.
+
+<eslint-code-block :rules="{'vue/require-explicit-slots': ['error']}">
+
+```vue
+<template>
+  <div>
+    <!-- ✓ GOOD -->
+    <slot />
+    <slot name="foo" />
+    <!-- ✗ BAD -->
+    <slot name="bar" />
+  </div>
+</template>
+<script setup lang="ts">
+defineSlots<{
+  default(props: { msg: string }): any
+  foo(props: { msg: string }): any
+}>()
+</script>
+```
+
+</eslint-code-block>
+
+<eslint-code-block :rules="{'vue/require-explicit-slots': ['error']}">
+
+```vue
+<template>
+  <div>
+    <!-- ✓ GOOD -->
+    <slot />
+    <slot name="foo" />
+    <!-- ✗ BAD -->
+    <slot name="bar" />
+  </div>
+</template>
+<script lang="ts">
+import { SlotsType } from 'vue'
+
+defineComponent({
+  slots: Object as SlotsType<{
+    default: { msg: string }
+    foo: { msg: string }
+  }>
+})
+</script>
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+
+Nothing.
diff --git a/lib/rules/require-explicit-slots.js b/lib/rules/require-explicit-slots.js
new file mode 100644
index 000000000..ed2d2edb9
--- /dev/null
+++ b/lib/rules/require-explicit-slots.js
@@ -0,0 +1,128 @@
+/**
+ * @author Mussin Benarbia
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const utils = require('../utils')
+
+/**
+ * @typedef {import('@typescript-eslint/types').TSESTree.TypeNode} TypeNode
+ */
+
+module.exports = {
+  meta: {
+    type: 'problem',
+    docs: {
+      description: 'require slots to be explicitly defined',
+      categories: undefined,
+      url: 'https://eslint.vuejs.org/rules/require-explicit-slots.html'
+    },
+    fixable: null,
+    schema: [],
+    messages: {
+      requireExplicitSlots: 'Slots must be explicitly defined.',
+      alreadyDefinedSlot: 'Slot {{slotName}} is already defined.'
+    }
+  },
+  /** @param {RuleContext} context */
+  create(context) {
+    const sourceCode = context.getSourceCode()
+    const documentFragment =
+      sourceCode.parserServices.getDocumentFragment &&
+      sourceCode.parserServices.getDocumentFragment()
+    if (!documentFragment) {
+      return {}
+    }
+    const scripts = documentFragment.children.filter(
+      (element) => utils.isVElement(element) && element.name === 'script'
+    )
+    if (scripts.every((script) => !utils.hasAttribute(script, 'lang', 'ts'))) {
+      return {}
+    }
+    const slotsDefined = new Set()
+
+    return utils.compositingVisitors(
+      utils.defineScriptSetupVisitor(context, {
+        onDefineSlotsEnter(node) {
+          const typeArguments =
+            'typeArguments' in node ? node.typeArguments : node.typeParameters
+          const param = /** @type {TypeNode|undefined} */ (
+            typeArguments?.params[0]
+          )
+          if (!param) return
+
+          if (param.type === 'TSTypeLiteral') {
+            for (const memberNode of param.members) {
+              const slotName = memberNode.key.name
+              if (slotsDefined.has(slotName)) {
+                context.report({
+                  node: memberNode,
+                  messageId: 'alreadyDefinedSlot',
+                  data: {
+                    slotName
+                  }
+                })
+              } else {
+                slotsDefined.add(slotName)
+              }
+            }
+          }
+        }
+      }),
+      utils.executeOnVue(context, (obj) => {
+        const slotsProperty = utils.findProperty(obj, 'slots')
+        if (!slotsProperty) return
+
+        const slotsTypeHelper =
+          slotsProperty.value.typeAnnotation?.typeName.name === 'SlotsType' &&
+          slotsProperty.value.typeAnnotation
+        if (!slotsTypeHelper) return
+
+        const typeArguments =
+          'typeArguments' in slotsTypeHelper
+            ? slotsTypeHelper.typeArguments
+            : slotsTypeHelper.typeParameters
+        const param = /** @type {TypeNode|undefined} */ (
+          typeArguments?.params[0]
+        )
+        if (!param) return
+
+        if (param.type === 'TSTypeLiteral') {
+          for (const memberNode of param.members) {
+            const slotName = memberNode.key.name
+            if (slotsDefined.has(slotName)) {
+              context.report({
+                node: memberNode,
+                messageId: 'alreadyDefinedSlot',
+                data: {
+                  slotName
+                }
+              })
+            } else {
+              slotsDefined.add(slotName)
+            }
+          }
+        }
+      }),
+      utils.defineTemplateBodyVisitor(context, {
+        "VElement[name='slot']"(node) {
+          let slotName = 'default'
+
+          const slotNameAttr = utils.getAttribute(node, 'name')
+
+          if (slotNameAttr) {
+            slotName = slotNameAttr.value.value
+          }
+
+          if (!slotsDefined.has(slotName)) {
+            context.report({
+              node,
+              messageId: 'requireExplicitSlots'
+            })
+          }
+        }
+      })
+    )
+  }
+}
diff --git a/tests/lib/rules/require-explicit-slots.js b/tests/lib/rules/require-explicit-slots.js
new file mode 100644
index 000000000..1849ea9a1
--- /dev/null
+++ b/tests/lib/rules/require-explicit-slots.js
@@ -0,0 +1,269 @@
+/**
+ * @author Mussin Benarbia
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/require-explicit-slots')
+
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: {
+    parser: require.resolve('@typescript-eslint/parser'),
+    ecmaVersion: 2020,
+    sourceType: 'module'
+  }
+})
+
+tester.run('require-explicit-slots', rule, {
+  valid: [
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <slot />
+        </div>
+      </template>
+      <script setup lang="ts">
+      defineSlots<{
+        default(props: { msg: string }): any
+      }>()
+      </script>`
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <slot></slot>
+        </div>
+      </template>
+      <script setup lang="ts">
+      defineSlots<{
+        default(props: { msg: string }): any
+      }>()
+      </script>`
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <slot name="foo"></slot>
+        </div>
+      </template>
+      <script setup lang="ts">
+      defineSlots<{
+        foo(props: { msg: string }): any
+      }>()
+      </script>`
+    },
+
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <slot />
+        </div>
+      </template>
+      <script lang="ts">
+      import { SlotsType } from 'vue'
+
+      defineComponent({
+        slots: Object as SlotsType<{
+          default: { msg: string }
+        }>,
+      })
+      </script>`
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <slot></slot>
+        </div>
+      </template>
+      <script lang="ts">
+      import { SlotsType } from 'vue'
+
+      defineComponent({
+        slots: Object as SlotsType<{
+          default: { msg: string }
+        }>,
+      })
+      </script>`
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <slot name="foo"></slot>
+        </div>
+      </template>
+      <script lang="ts">
+      import { SlotsType } from 'vue'
+
+      defineComponent({
+        slots: Object as SlotsType<{
+          foo(props: { msg: string }): any
+        }>,
+      })
+      </script>`
+    },
+    // does not report any error if the script is not TS
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <slot name="foo"></slot>
+        </div>
+      </template>
+      <script setup>
+      </script>`,
+      parserOptions: {
+        parser: null
+      }
+    }
+  ],
+  invalid: [
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <slot />
+        </div>
+      </template>
+      <script setup lang="ts">
+      </script>`,
+      errors: [
+        {
+          message: 'Slots must be explicitly defined.'
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <slot></slot>
+        </div>
+      </template>
+      <script setup lang="ts">
+      </script>`,
+      errors: [
+        {
+          message: 'Slots must be explicitly defined.'
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <slot name="foo" />
+        </div>
+      </template>
+      <script setup lang="ts">
+      </script>`,
+      errors: [
+        {
+          message: 'Slots must be explicitly defined.'
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <slot name="foo" />
+        </div>
+      </template>
+      <script setup lang="ts">
+      defineSlots<{
+        default(props: { msg: string }): any
+      }>()
+      </script>`,
+      errors: [
+        {
+          message: 'Slots must be explicitly defined.'
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <slot name="foo" />
+        </div>
+      </template>
+      <script lang="ts">
+      import { SlotsType } from 'vue'
+
+      defineComponent({
+        slots: Object as SlotsType<{
+          default: { msg: string }
+        }>,
+      })
+      </script>`,
+      errors: [
+        {
+          message: 'Slots must be explicitly defined.'
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <slot name="foo" />
+        </div>
+      </template>
+      <script setup lang="ts">
+      defineSlots<{
+        foo(props: { msg: string }): any
+        foo(props: { msg: string }): any
+      }>()
+      </script>`,
+      errors: [
+        {
+          message: 'Slot foo is already defined.'
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <slot name="foo" />
+        </div>
+      </template>
+      <script setup lang="ts">
+      defineSlots<{
+        foo(props: { msg: string }): any
+      }>()
+      defineSlots<{
+        default(props: { msg: string }): any,
+        foo(props: { msg: string }): any
+      }>()
+      </script>`,
+      errors: [
+        {
+          message: 'Slot foo is already defined.'
+        }
+      ]
+    }
+  ]
+})