diff --git a/CHANGELOG.md b/CHANGELOG.md
index 65ca7558d..7456d004f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@ Changelog for NeoFS Node
 
 ### Added
 - Config examples for Inner ring application (#1358)
+- Command for documentation generation for `neofs-cli`, `neofs-adm` and `neofs-lens` (#1396)
 
 ### Fixed
 - Do not ask for contract wallet password twice (#1346)
diff --git a/cmd/neofs-adm/internal/modules/root.go b/cmd/neofs-adm/internal/modules/root.go
index 4322e6c70..2234fc3d6 100644
--- a/cmd/neofs-adm/internal/modules/root.go
+++ b/cmd/neofs-adm/internal/modules/root.go
@@ -9,6 +9,7 @@ import (
 	"github.com/nspcc-dev/neofs-node/cmd/neofs-adm/internal/modules/storagecfg"
 	"github.com/nspcc-dev/neofs-node/misc"
 	"github.com/nspcc-dev/neofs-node/pkg/util/autocomplete"
+	"github.com/nspcc-dev/neofs-node/pkg/util/gendoc"
 	"github.com/spf13/cobra"
 	"github.com/spf13/viper"
 )
@@ -42,6 +43,7 @@ func init() {
 	rootCmd.AddCommand(storagecfg.RootCmd)
 
 	rootCmd.AddCommand(autocomplete.Command("neofs-adm"))
+	rootCmd.AddCommand(gendoc.Command(rootCmd))
 }
 
 func Execute() error {
diff --git a/cmd/neofs-cli/modules/root.go b/cmd/neofs-cli/modules/root.go
index 11a7d5851..49b798e1c 100644
--- a/cmd/neofs-cli/modules/root.go
+++ b/cmd/neofs-cli/modules/root.go
@@ -18,6 +18,7 @@ import (
 	bearerCli "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/modules/bearer"
 	sessionCli "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/modules/session"
 	"github.com/nspcc-dev/neofs-node/misc"
+	"github.com/nspcc-dev/neofs-node/pkg/util/gendoc"
 	"github.com/nspcc-dev/neofs-sdk-go/bearer"
 	"github.com/nspcc-dev/neofs-sdk-go/client"
 	"github.com/nspcc-dev/neofs-sdk-go/owner"
@@ -93,6 +94,7 @@ func init() {
 	rootCmd.AddCommand(bearerCli.Cmd)
 	rootCmd.AddCommand(sessionCli.Cmd)
 	rootCmd.AddCommand(accountingCli.Cmd)
+	rootCmd.AddCommand(gendoc.Command(rootCmd))
 }
 
 func entryPoint(cmd *cobra.Command, _ []string) {
diff --git a/cmd/neofs-lens/root.go b/cmd/neofs-lens/root.go
index f27ae7744..8cfdd9c39 100644
--- a/cmd/neofs-lens/root.go
+++ b/cmd/neofs-lens/root.go
@@ -7,6 +7,7 @@ import (
 	"github.com/nspcc-dev/neofs-node/cmd/neofs-lens/internal/commands/inspect"
 	cmdlist "github.com/nspcc-dev/neofs-node/cmd/neofs-lens/internal/commands/list"
 	"github.com/nspcc-dev/neofs-node/misc"
+	"github.com/nspcc-dev/neofs-node/pkg/util/gendoc"
 	"github.com/spf13/cobra"
 )
 
@@ -36,6 +37,7 @@ func init() {
 	command.AddCommand(
 		cmdlist.Command,
 		inspect.Command,
+		gendoc.Command(command),
 	)
 }
 
diff --git a/go.mod b/go.mod
index b7044cf26..55b3304ef 100644
--- a/go.mod
+++ b/go.mod
@@ -26,6 +26,7 @@ require (
 	github.com/prometheus/client_golang v1.11.0
 	github.com/spf13/cast v1.3.1
 	github.com/spf13/cobra v1.1.3
+	github.com/spf13/pflag v1.0.5
 	github.com/spf13/viper v1.8.1
 	github.com/stretchr/testify v1.7.0
 	go.etcd.io/bbolt v1.3.6
@@ -80,7 +81,6 @@ require (
 	github.com/spaolacci/murmur3 v1.1.0 // indirect
 	github.com/spf13/afero v1.6.0 // indirect
 	github.com/spf13/jwalterweatherman v1.1.0 // indirect
-	github.com/spf13/pflag v1.0.5 // indirect
 	github.com/subosito/gotenv v1.2.0 // indirect
 	github.com/syndtr/goleveldb v1.0.1-0.20210305035536-64b5b1c73954 // indirect
 	github.com/twmb/murmur3 v1.1.5 // indirect
diff --git a/pkg/util/gendoc/gendoc.go b/pkg/util/gendoc/gendoc.go
new file mode 100644
index 000000000..59c36fd34
--- /dev/null
+++ b/pkg/util/gendoc/gendoc.go
@@ -0,0 +1,144 @@
+package gendoc
+
+import (
+	"errors"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"strings"
+	"text/template"
+
+	"github.com/spf13/cobra"
+	"github.com/spf13/cobra/doc"
+	"github.com/spf13/pflag"
+)
+
+const (
+	gendocTypeFlag = "type"
+
+	gendocMarkdown = "md"
+	gendocMan      = "man"
+
+	depthFlag     = "depth"
+	extensionFlag = "extension"
+)
+
+// Command returns command which generates user documentation for the argument.
+func Command(rootCmd *cobra.Command) *cobra.Command {
+	gendocCmd := &cobra.Command{
+		Use:   "gendoc <dir>",
+		Short: "Generate documentation for this command.",
+		Long: `Generate documentation for this command. If the template is not provided,
+builtin cobra generator is used and each subcommand is placed in
+a separate file in the same directory.
+
+The last optional argument specifies the template to use with text/template.
+In this case there is a number of helper functions which can be used:
+  replace STR FROM TO -- same as strings.ReplaceAll
+  join ARRAY SEPARATOR -- same as strings.Join
+  split STR SEPARATOR -- same as strings.Split
+  fullUse CMD -- slice of all command names starting from the parent
+  listFlags CMD -- list of command flags
+`,
+		RunE: func(cmd *cobra.Command, args []string) error {
+			if len(args) == 0 {
+				_ = cmd.Usage()
+				os.Exit(1)
+			}
+
+			err := os.MkdirAll(args[0], os.ModePerm)
+			if err != nil {
+				return fmt.Errorf("can't create directory: %w", err)
+			}
+
+			if len(args) == 2 {
+				data, err := ioutil.ReadFile(args[1])
+				if err != nil {
+					return fmt.Errorf("can't read the template '%s': %w", args[1], err)
+				}
+
+				return generateTemplate(cmd, rootCmd, args[0], data)
+			}
+
+			typ, _ := cmd.Flags().GetString(gendocTypeFlag)
+			switch typ {
+			case gendocMarkdown:
+				return doc.GenMarkdownTree(rootCmd, args[0])
+			case gendocMan:
+				hdr := &doc.GenManHeader{
+					Section: "1",
+					Source:  "NSPCC & Morphbits",
+				}
+				return doc.GenManTree(rootCmd, hdr, args[0])
+			default:
+				return errors.New("type must be 'md' or 'man'")
+			}
+		},
+	}
+
+	ff := gendocCmd.Flags()
+	ff.StringP(gendocTypeFlag, "t", gendocMarkdown, "type for the documentation ('md' or 'man')")
+	ff.Int(depthFlag, 1, "if template is specified, unify all commands starting from depth in a single file. Default: 1.")
+	ff.StringP(extensionFlag, "e", "", "if the template is specified, string to append to the output file names")
+	return gendocCmd
+}
+
+func generateTemplate(cmd *cobra.Command, rootCmd *cobra.Command, outDir string, tmpl []byte) error {
+	depth, _ := cmd.Flags().GetInt(depthFlag)
+	ext, _ := cmd.Flags().GetString(extensionFlag)
+
+	tm := template.New("doc")
+	tm.Funcs(template.FuncMap{
+		"replace":   strings.ReplaceAll,
+		"split":     strings.Split,
+		"join":      strings.Join,
+		"fullUse":   fullUse,
+		"listFlags": listFlags,
+	})
+
+	tm, err := tm.Parse(string(tmpl))
+	if err != nil {
+		return err
+	}
+
+	return visit(rootCmd, outDir, ext, depth, tm)
+}
+
+func visit(rootCmd *cobra.Command, outDir string, ext string, depth int, tm *template.Template) error {
+	if depth == 0 {
+		name := strings.Join(fullUse(rootCmd), "-")
+		name = strings.TrimSpace(name)
+		name = strings.ReplaceAll(name, " ", "-")
+		name = filepath.Join(outDir, name) + ext
+		f, err := os.Create(name)
+		if err != nil {
+			return fmt.Errorf("can't create file '%s': %w", name, err)
+		}
+		defer f.Close()
+		return tm.Execute(f, rootCmd)
+	}
+
+	for _, c := range rootCmd.Commands() {
+		err := visit(c, outDir, ext, depth-1, tm)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func fullUse(c *cobra.Command) []string {
+	if c == nil {
+		return nil
+	}
+	return append(fullUse(c.Parent()), c.Name())
+}
+
+func listFlags(c *cobra.Command) []*pflag.Flag {
+	var res []*pflag.Flag
+	c.Flags().VisitAll(func(f *pflag.Flag) {
+		res = append(res, f)
+	})
+	return res
+}